확률론적 언어 모형(Probabilistic Language Model)은 $m$개의 단어 $w_1, w_2, \ldots, w_m$ 열(word sequence)이 주어졌을 때 문장으로써 성립될 확률 $P(w_1, w_2, \ldots, w_m)$ 을 출력함으로써 이 단어 열이 실제로 현실에서 사용될 수 있는 문장(sentence)인지를 판별하는 모형이다.
이 확률은 각 단어의 확률과 단어들의 조건부 확률을 이용하여 다음과 같이 계산할 수 있다.
$$ \begin{eqnarray} P(w_1, w_2, \ldots, w_m) &=& P(w_1, w_2, \ldots, w_{m-1}) \cdot P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \\ &=& P(w_1, w_2, \ldots, w_{m-2}) \cdot P(w_{m-1}\;|\; w_1, w_2, \ldots, w_{m-2}) \cdot P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \\ &=& P(w_1) \cdot P(w_2 \;|\; w_1) \cdot P(w_3 \;|\; w_1, w_2) P(w_4 \;|\; w_1, w_2, w_3) \cdots P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \end{eqnarray} $$여기에서 $P(w_m\;|\; w_1, w_2, \ldots, w_{m-1})$ 은 지금까지 $w_1, w_2, \ldots, w_{m-1}$라는 단어 열이 나왔을 때, 그 다음 단어로 $w_m$이 나올 조건부 확률을 말한다. 여기에서 지금까지 나온 단어를 문맥(context) 정보라고 한다.
이 때 조건부 확률을 어떻게 모형화하는냐에 따라
등으로 나뉘어 진다.
실제 텍스트 코퍼스(corpus)에서 확률을 추정하는 방법은 다음과 같다. 여기에서는 바이그램의 경우를 살펴본다.
일단 모든 문장에 문장의 시작과 끝을 나타내는 특별 토큰을 추가한다. 예를 들어 문장의 시작은 SS
, 문장의 끝은 SE
이라는 토큰을 사용할 수 있다.
바이그램 모형에서는 전체 문장의 확률은 다음과 같이 조건부 확률의 곱으로 나타난다.
$$ P(\text{SS I am a boy SE}) = P(\text{I}\;|\; \text{SS}) \cdot P(\text{am}\;|\; \text{I}) \cdot P(\text{a}\;|\; \text{am}) \cdot P(\text{boy}\;|\; \text{a}) \cdot P(\text{SE}\;|\; \text{boy}) $$조건부 확률은 다음과 같이 추정한다.
$$ P(w_{i}\;|\; w_{i-1}) = \dfrac{C(w_{i}, w_{i-1})}{C(w_{i-1})} $$위 식에서 $C(w_{i}, w_{i-1})$은 전체 코퍼스에서 $(w_{i}, w_{i-1})$라는 바이그램이 나타나는 횟수이고 $C(w_{i-1})$은 전체 코퍼스에서 $(w_{i-1})$라는 유니그램(단어)이 나타나는 횟수이다.
다음은 nltk 패키지의 샘플 코퍼스인 movie_reviews의 텍스트를 기반으로 N-그램 모형을 추정하고 모형 확률로부터 랜덤하게 문장을 생성하는 예제이다. 다음 문헌을 참고하여 일부 수정하였다.
우선 다음과 같이 문장(단어 리스트)의 리스트를 만든다.
In [7]:
from nltk.corpus import movie_reviews
# 문서를 문장으로 분리
sentences = list(movie_reviews.sents())
import random
# 섞는다.
random.seed(1)
random.shuffle(sentences)
In [8]:
sentences[0]
Out[8]:
이제 이 입력으로부터 확률값을 추정한다.
In [9]:
import collections, math
from math import log
from collections import Counter
from konlpy.utils import pprint
def stringify_context(context):
return(" ".join(context))
boundaryToken = "</s>"
def ngrams(n, sentences, boundaryToken=boundaryToken, verbose=False):
c = {}
q = []
for i in range(n-1):
q.append(boundaryToken)
for sentence in sentences:
for w in sentence + [boundaryToken]:
context_gram = stringify_context(q)
if verbose:
print(q)
print(context_gram)
print(w)
if not context_gram in c:
c[context_gram] = Counter()
c[context_gram][w] += 1
q.pop(0)
q.append(w)
return(c)
In [15]:
ngrams(2, sentences[:1000])["we"]
Out[15]:
In [47]:
class BigramModel:
def __init__(self, training_sentences, smoothing='none'):
train = ngrams(2, training_sentences)
self.probs = {}
if smoothing == 'none':
for context_gram in train.keys():
N = sum(train[context_gram].values())
self.probs[context_gram] = Counter({k:v/N for k,v in train[context_gram].items()})
def prob(self, word, context):
"""takes a word string and a context which is a list of word strings, and returns the probability of the word"""
c = stringify_context(context)
return(self.probs[c][word])
def scoreSentence(self, sentence, verbose=False):
context = [boundaryToken]
result = 0
for w in sentence + [boundaryToken]:
lp = log(self.prob(w, context))
result = result + lp
if verbose:
pprint([context, w, lp])
context = [w]
return result
def generateSentence(self, verbose=False, goryDetails=False):
context = [boundaryToken]
result = []
w = None
while not w == boundaryToken:
r = random.random() # returns a random float between 0 and 1
x = 0
c = self.probs[stringify_context(context)] # this will be a Counter
w = c.keys()[np.argmax(np.random.multinomial(1, c.values(), (1,))[0])]
result.append(w)
context = [w]
if verbose:
print(w)
result.pop() # drop the boundary token
return result
In [46]:
m = BigramModel(sentences)
트레이닝이 끝나면 조건부 확률의 값을 보거나 샘플 문장을 입력해서 문장의 로그 확률을 구할 수 있다.
"i" 라는 단어가 나온 뒤에 "am"이라는 단어가 나올 확률을 계산하면
In [24]:
m.prob("am", ["i"])
Out[24]:
In [6]:
m.prob("</s>", ["."]) # .(마침표) 뒤에 문장이 끝날 확률
Out[6]:
In [23]:
m.probs["."]
Out[23]:
In [6]:
m.prob("the", ["in"]) # in 뒤에 the 가 올 확률
Out[6]:
In [7]:
m.prob("in", ["the"]) # the 뒤에 in 이 올 확률
Out[7]:
In [25]:
test_sentence = ['in', 'the', '1970s', '.']
m.scoreSentence(test_sentence, verbose=True)
Out[25]:
In [26]:
m.scoreSentence(["i", "am", "a", "boy", "."], verbose=True)
Out[26]:
이 모형을 기반으로 임의의 랜덤한 샘플 즉, 문장을 생성해 보면 다음과 같다.
여기에서는 하나의 단어부터 시작하여 문장의 확률이 난수값보다 크게 만드는 첫번째 단어를 찾아서 이어붙이는 방식을 취했다. 만약 최저 확률을 높이면 올바른 단어를 찾는 시간이 더 오래 걸리게 된다.
In [44]:
random.seed(1)
print(" ".join(m.generateSentence()))
이번에는 한글 자료를 이용해보자 코퍼스로는 아래의 웹사이트에 공개된 Naver sentiment movie corpus 자료를 사용한다.
In [48]:
import codecs
def read_data(filename):
with codecs.open(filename, encoding='utf-8', mode='r') as f:
data = [line.split('\t') for line in f.read().splitlines()]
data = data[1:] # header 제외
return data
train_data = read_data('/home/dockeruser/data/nsmc/ratings_train.txt')
In [49]:
from konlpy.tag import Twitter
tagger = Twitter()
def tokenize(doc):
return ['/'.join(t) for t in tagger.pos(doc, norm=True, stem=True)]
train_docs = [row[1] for row in train_data]
sentences = [tokenize(d) for d in train_docs]
In [50]:
m = BigramModel(sentences)
In [51]:
m.prob(tokenize(u"영화")[0], tokenize(u"이"))
Out[51]:
In [52]:
m.scoreSentence(tokenize(u"이 영화 정말 좋네"), verbose=True)
Out[52]:
In [54]:
m.scoreSentence(tokenize(u"좋네 영화 이 정말"), verbose=True)
Out[54]:
In [98]:
random.seed(24)
print("".join([w.split("/")[0] if w.split("/")[1] == "Josa" else " " + w.split("/")[0] for w in m.generateSentence()]))
확률론적 언어 모형은 다음과 같은 분야에 광범위하게 활용할 수 있다.